/*-*-C-*-
 * Window keyboard shortcuts dialogue box for Windows CSE
 */

#include "resed.h"
#include "main.h"

#include "swicall.h"
#include "wimp.h"
#include "resformat.h"
#include "newmsgs.h"
#include "dbox.h"
#include "interactor.h"
#include "registry.h"

#include "format.h"
#include "windowedit.h"
#include "gui.h"
#include "icondefs.h"
#include "keycuts.h"
#include "protocol.h"


/*
 * Terminology:
 *
 *  The keyboard shortcuts ("keycuts" for short) dialogue box consists of:
 *    - a "parent dbox" which contains the display and writable icons and
 *       action buttons, and
 *    - a scrolling "pane" which sits on top of the parent dbox and contains
 *       the list of keycuts.
 */


static WindowPtr parentproto, paneproto;
static int parentsize, panesize;

/* these are needed when painting selected keycuts in the pane */
static int panefg, panebg;

/* these colours are used as backgrounds for the KEY icon */
#define  ON_COLOUR  (0)       /* KEY icon available for direct input */
#define  OFF_COLOUR (1)       /* KEY icon not available */

#define  HIGHLIGHT_MASK  (IF_BG_MASK << IF_BG_SHFT)
#define  FLAGS_ON        (ON_COLOUR   << IF_BG_SHFT)
#define  FLAGS_OFF       (OFF_COLOUR  << IF_BG_SHFT)


/* used to determine whether to use Wimp_TextOp - or just OS_Write0 */
static Bool usetextop;
static int line_height;   /* takes one of the following two fixed values */

#define  SYSTEM_FONT_HEIGHT          32    /* for "fixed" Wimps */
#define  OUTLINE_SYSTEM_FONT_HEIGHT  36    /* for "fonting" Wimps */

#define  TEXT_OP_FUDGE  8  /* This is the number of OS units by which the
                              bottom left y co-ordinate of Wimp_TextOp 2
                              must be increased so that the text actually
                              lies within the "line" */



/*
 * Load any templates required - called from load_prototypes(..) in main.c
 */

error * keycuts_load_prototypes (void)

{
    ER ( wimp_load_template_returning_size (
                  "KeyParent", &parentproto, &parentsize) );
    ER ( wimp_load_template_returning_size (
                  "KeyPane", &paneproto, &panesize) );

    /* note pane's work area colours - needed during redraw to highlight */
    panefg = paneproto->colours.workFG;
    panebg = paneproto->colours.workBG;

    /* determine whether Wimp_TextOp can be used */
    usetextop = (wimpversion >= 350);

    /* and select an appropriate line height */
    line_height = (usetextop) ? OUTLINE_SYSTEM_FONT_HEIGHT
                              : SYSTEM_FONT_HEIGHT;

    return NULL;
}


/*
 * Translates a key code into its name.
 */

static char * code_to_keyid (int code)
{
    static char name[12];
    char *prefix = "";
    Bool upper = FALSE;
    char *keyname = "";
    int origcode = code;

    /* "normal" keys */
    if (code >= 32 && code <= 255)
    {
        if (code == 0x7f)
            return "DELETE";

        name[0] = code;
        name[1] = 0;
        return name;
    }

    /* control keys */
    if (code >= 0 && code <= 26)
    {
        name[0] = '^';
        name[1] = (code == 0) ? '0' : 'A' + (code - 1);
        name[2] = 0;
        return name;
    }

    /* special keys */
    if (code == 0x1b)
        return "ESC";

    if (code == 0x1e)
        return "HOME";

    if (code >= 0x1c0 && code <= 0x1ff)
    {
        upper = TRUE;
        code -= 0x40;
    }
    if (code >= 0x180 && code <= 0x1bf)
    {
        if (code >= 0x1b0)
        {
            code -= 0x30;
            prefix = "^";
        }    
        if (code >= 0x1a0)
        {
            code -= 0x20;
            prefix = "^";
        }
        if (code >= 0x190)
        {
            code -= 0x10;
            prefix = "";
        }
    }
    if (upper)
        code += 0x40;

    if (code >= 0x181 && code <= 0x189 ||
        code >= 0x1ca && code <= 0x1cc)
    {
        sprintf (name, "%sF%d", prefix, code & 0xf);
        return name;
    }

    switch (code)
    {
    case 0x180:  keyname = "PRINT"; break;
    case 0x18a:  keyname = "TAB"; break;
    case 0x18b:  keyname = "COPY"; break;
    case 0x18c:  keyname = "LEFT"; break;
    case 0x18d:  keyname = "RIGHT"; break;
    case 0x18e:  keyname = "DOWN"; break;
    case 0x18f:  keyname = "UP"; break;
    case 0x1cd:  keyname = "INSERT"; break;
    }

    if (*keyname != 0)
    {
        strcpy (name, prefix);
        strcat (name, keyname);
        return name;
    }

    /* don't know this one! */
    sprintf(name, "&%x", origcode);
    return name;
}


/*
 * Free space occupied by a list of keycuts
 */

static void free_keycuts (KeyBoardShortcutPtr key)
{
    while (key != NULL)
    {
        KeyBoardShortcutPtr next = key->next;
        free (key->keyshow);
        free (key);
        key = next;
    }
    return;
}


/*
 * Make a copy of a list of keycuts - leaving 'selected' FALSE
 */

static error * copy_keycuts (
    KeyBoardShortcutPtr original,
    KeyBoardShortcutPtr *copy
)
{
    KeyBoardShortcutPtr new;

    *copy = NULL;
    while (original != NULL)
    {
        new = calloc (1, sizeof (KeyBoardShortcutRec));
        if (new == NULL)
            return error_lookup ("NoMem");

        *copy = new;
        copy = &new->next;

        new->flags = original->flags;
        new->wimpkeycode = original->wimpkeycode;
        new->keyevent = original->keyevent;
        if (original->keyshow != NULL)
        {
            int n = strlen (original->keyshow) + 1;
            char *s = malloc (n);
            if (s == NULL)
                return error_lookup ("NoMem");
            strcpy (s, original->keyshow);
            new->keyshow = s;
        }

        original = original->next;
    }

    return NULL;

}


/*
 * Reset pane's extent
 */

static error * set_pane_extent (WindowObjPtr window)
{
    WindowPtr pane = window->keycutspane;
    int minheight = pane->visarea.maxy - pane->visarea.miny;
    int height = line_height * (window->numkeys + 1);

    if (height < minheight)
        height = minheight;

    pane->workarea.miny = pane->workarea.maxy - height;
    return ( swi (Wimp_SetExtent, R0, pane->handle,
                                  R1, &pane->workarea, END) );
}


/*
 * (Re-)initialise the keycuts dbox:
 *   - throw away any existing copy of the keycuts list
 *   - make a fresh copy (with 'selected' fields FALSE)
 *   - set pane's extent according to number of keycuts present
 *   - initialise keycuts dbox icons
 */

static error * keycuts_init_dbox (WindowObjPtr window)
{
    WindowPtr dbox = window->keycutsdbox;

    /* throw away any current local copy of the keycuts list */
    free_keycuts (window->keys);

    /* and make a fresh copy */
    ER ( copy_keycuts (window->keyboardshortcuts, &window->keys) );
    window->numkeys = window->numkeyboardshortcuts;
    window->numkeysselected = 0;

    /* initialise icons */
    dbox_setstring (dbox, I_KEYPARENT_KEY, "");
    dbox_setstring (dbox, I_KEYPARENT_CODE, "");
    gui_put_event (dbox, I_KEYPARENT_DELIVER, I_KEYPARENT_EVENT, 0);
    gui_put_opt_str (dbox, I_KEYPARENT_SHOW, I_KEYPARENT_OBJECT, NULL);
    dbox_setbutton (dbox, I_KEYPARENT_TRANSIENT, FALSE);
    dbox_shade (dbox, I_KEYPARENT_TRANSIENT, TRUE);
    dbox_shade (dbox, I_KEYPARENT_DELETE, TRUE);

    /* set the pane's extent as appropriate */
    ER ( set_pane_extent (window) );

    /* reset vertical scroll offset to 0 */
    window->keycutspane->scrolloffset.y = 0;

    return NULL;
}


/*
 * Called by keycuts_dbox_mouse_click(..) or keycuts_dbox_key_pressed(..)
 * after the user has indicated that he wishes to apply the contents of the
 * dbox.
 *
 */

static error * keycuts_apply_dbox (WindowObjPtr window, Bool stayopen)
{
    Bool modified = TRUE;

    /* check to see if any changes have been made */
    if (window->numkeyboardshortcuts == window->numkeys)
    {
        KeyBoardShortcutPtr old = window->keyboardshortcuts;
        KeyBoardShortcutPtr new = window->keys;

        while (old)
        {
            if ( old->flags != new->flags ||
                 old->wimpkeycode != new->wimpkeycode ||
                 old->keyevent != new->keyevent ||
                 !equalstrings (old->keyshow, new->keyshow) )
                break;
            old = old->next;
            new = new->next;
        }

        if (old == NULL)
            modified = FALSE;
    }

    /* copy list of keycuts back to window object */
    free_keycuts (window->keyboardshortcuts);
    copy_keycuts (window->keys, &window->keyboardshortcuts);
    window->numkeyboardshortcuts = window->numkeys;

    /* inform shell that window object has been modified */
    if (modified)
        protocol_send_resed_object_modified (window);

    /* destroy keycuts dbox if necessary */
    if (!stayopen)
        return keycuts_close_dbox (window);

    return NULL;
}


/*
 * Open the pane at an appropriate position relative to the parent, and
 *  behind the same window as the parent is behind (opening a window behind
 *  itself seems to be acceptable to the Wimp!).
 */

static error * open_pane (WindowPtr pane, WindowPtr parent)
{
    int width = pane->visarea.maxx - pane->visarea.minx;
    int height = pane->visarea.maxy - pane->visarea.miny;

    pane->visarea.minx = parent->visarea.minx;
    pane->visarea.maxy = parent->visarea.maxy;
    pane->visarea.maxx = pane->visarea.minx + width;
    pane->visarea.miny = pane->visarea.maxy - height;

    pane->behind = parent->behind;

    return swi (Wimp_OpenWindow, R1, pane, END);
}


/*
 * Open the parent window behind the pane - and reopen the pane if the Wimp
 *  moves the parent (ie to force on-screen).
 */

static error * open_parent (WindowPtr pane, WindowPtr parent)
{
    RectRec pos = * ((RectPtr) &parent->visarea);  /* copy of intended pos */

    parent->behind = pane->handle;
    ER ( swi (Wimp_OpenWindow, R1, parent, END) );

    /* if opening the parent moved it, must reopen the pane */
    if ( pos.minx != parent->visarea.minx ||
         pos.miny != parent->visarea.miny ||
         pos.maxx != parent->visarea.maxx ||
         pos.maxy != parent->visarea.maxy )
            ER ( open_pane (pane, parent) );

    return NULL;
}


/*
 * Update keycuts dbox - only called if the dbox (both parent and pane)
 *  exists.
 *
 * Called from windowedit_rename_window(..) - when the object is renamed.
 *        from windowedit_load(..) - when a new version of the object is
 *                                    force loaded [not at present]
 *        from keycuts_dbox_mouse_click(..) - after an ADJ-CANCEL click
 */

error * keycuts_update_dbox (
    WindowObjPtr window,
    Bool contents,
    Bool title
)
{
    if (contents)
    {
        WindowPtr pane = window->keycutspane;

        /* retrieve details of current state before closing */
        ER ( swi (Wimp_GetWindowState, R1, pane, END) );

        /* re-initialise dbox and related structures */
        ER ( keycuts_init_dbox (window) );

        /* close and reopen pane */
        ER ( swi (Wimp_CloseWindow, R1, pane, END) );
        ER ( swi (Wimp_OpenWindow, R1, pane, END) );
    }

    if (title)
    {
        /* Get the printf pattern from the prototype's titlebar which
         * should have %s in it where the name is wanted
         */
        char buf[256];
        sprintf (buf, dbox_gettitle (parentproto), window->name);
        ER ( dbox_settitle (window->keycutsdbox, buf, TRUE) );
    }

    return NULL;
}


/*
 * Called from:
 *  windowedit_menu_cb(..) - when "Key shortcuts..." is chosen;
 *  windowedit_key_pressed(..) - when ^K is pressed
 *
 * If the corresponding keycuts dbox is already open, it is
 *  simply raised to the top of the window stack.
 *
 * Otherwise, a new keycuts dbox - pane and parent - is created and opened.
 *
 * Any new window will be displayed at the same position as the most recent
 *  position of any other window created from the "KeyParent" template;
 *  this is achieved by copying co-ordinates to the template whenever an
 *  existing window is reopened - see keycuts_reopen_dbox(..).
 */

error * keycuts_open_dbox (WindowObjPtr window)
{
    if (window->keycutsdbox == NULL)
    {
        /* create and register the pane and parent windows */
        ER ( wimp_copy_template (paneproto,
                                 &window->keycutspane, panesize) );
        ER ( swi (Wimp_CreateWindow,  R1, &window->keycutspane->visarea,
                        OUT,  R0, &window->keycutspane->handle,  END) );
        ER ( registry_register_window (window->keycutspane->handle,
                                       ShortcutsPane, (void *) window) );
        ER ( wimp_copy_template (parentproto,
                                 &window->keycutsdbox, parentsize) );
        ER ( swi (Wimp_CreateWindow,  R1, &window->keycutsdbox->visarea,
                        OUT,  R0, &window->keycutsdbox->handle,  END) );
        ER ( registry_register_window (window->keycutsdbox->handle,
                                       ShortcutsDbox, (void *) window) );

        /* create copy of keycuts list, and initialise templates */
        ER ( keycuts_init_dbox (window) );

        /* open the pane first, then the parent window */
        window->keycutsdbox->behind = -1; /* on top */
        ER ( open_pane (window->keycutspane, window->keycutsdbox) );
        ER ( open_parent (window->keycutspane, window->keycutsdbox) );

        /* get the title correct */
        keycuts_update_dbox (window, FALSE, TRUE);
    }
    else  /* dbox already exists - just raise to top of stack */
    {
        window->keycutsdbox->behind = -1;
        ER ( open_pane (window->keycutspane, window->keycutsdbox) );
        ER ( open_parent (window->keycutspane, window->keycutsdbox) );
    }   

    /* give input focus to the parent window */
    return dbox_set_caret_to (window->keycutsdbox, -1);
}


/*
 * Called to invalidate a line of the pane
 */

static void redraw_line (WindowPtr pane, int i)
{
    int topy = - (i * line_height);

    swi (Wimp_ForceRedraw, R0, pane->handle,
                           R1, pane->workarea.minx,
                           R2, topy - line_height,
                           R3, pane->workarea.maxx,
                           R4, topy, END);
    return;
}


/*
 * Called when the UPDATE action button is clicked.
 */

static error * update_keycuts (WindowObjPtr window)
{
    WindowPtr dbox = window->keycutsdbox;
    int newkeycode;
    KeyBoardShortcutPtr key = window->keys;
    int line;

    /* a key code must be specified ... */
    if (*(dbox_getstring (dbox, I_KEYPARENT_CODE)) == 0)  /* empty */
        return error_lookup ("Nokeycode");

    /* ... and either an event to raise or an object to show */
    if ( !dbox_getbutton (dbox, I_KEYPARENT_DELIVER) &&
         !dbox_getbutton (dbox, I_KEYPARENT_SHOW) )
        return error_lookup ("Nokeyact");

    /* set KEY field from CODE field */
    newkeycode = dbox_getint (dbox, I_KEYPARENT_CODE);
    if (newkeycode >= 0x200)
        return error_lookup ("Badkeycode");
    dbox_setstring (dbox, I_KEYPARENT_KEY, code_to_keyid (newkeycode));

    /* see if this is a replacement or addition */
    line = 0;
    while (key != NULL && key->wimpkeycode != newkeycode)
    {
        line++;
        key = key->next;
    }

    /* create new keyboard shortcut record if necessary */
    if (key == NULL)
    {
        /* allocate space */
        key = (KeyBoardShortcutPtr) calloc (1, sizeof (KeyBoardShortcutRec));
        if (key == NULL)
            return error_lookup ("NoMem");

        /* add to end of keycuts list */
        {
            KeyBoardShortcutPtr *keylist = &window->keys;

            while (*keylist != NULL)
                keylist = &((*keylist)->next);

            *keylist = key;

            /* increment number of keycuts, and reset pane's extent */
            window->numkeys++;
            set_pane_extent (window);

            /* scroll to the bottom to show new entry */
            {
                WindowPtr pane = window->keycutspane;
                pane->scrolloffset.y = pane->workarea.miny +
                                       pane->visarea.maxy -
                                       pane->visarea.miny;
                /* and reopen the pane */
                swi (Wimp_OpenWindow, R1, pane, END);
            }
        }
    }

    /* set the keycut record's fields from the dialogue box */
    key->wimpkeycode = newkeycode;
    key->keyevent =
        gui_get_event (dbox, I_KEYPARENT_DELIVER, I_KEYPARENT_EVENT);
    ER ( gui_get_opt_str (dbox,
                          I_KEYPARENT_SHOW, I_KEYPARENT_OBJECT,
                          &key->keyshow) );
    GUI_GET_FLAG(dbox, I_KEYPARENT_TRANSIENT, key->flags,
                                       KEYBOARDSHORTCUT_SHOWASTRANSIENT);

    /* force redraw of this shortcut */
    redraw_line (window->keycutspane, line);

    return NULL;
}


/*
 * Delete all selected keycut records from the dbox
 */

static error * delete_keycuts (WindowObjPtr window)
{
    KeyBoardShortcutPtr *key = &window->keys;
    int n = 0;
    WindowPtr pane = window->keycutspane;

    /* destroy all selected keycut records */
    while (*key != NULL)
    {
        KeyBoardShortcutPtr this = *key;
        if (this->selected)
        {
            *key = this->next;
            free (this->keyshow);
            free (this);
        }
        else
        {
            n++;
            key = &this->next;
        }
    }

    /* update status variables */
    window->numkeys = n;
    window->numkeysselected = 0;

    /* invalidate entire work area (lazybones!) */
    swi (Wimp_ForceRedraw, R0, pane->handle,
                           R1, pane->workarea.minx,
                           R2, pane->workarea.miny,
                           R3, pane->workarea.maxx,
                           R4, pane->workarea.maxy, END);

    /* reset pane's extent */
    set_pane_extent (window);

    /* and re-open the pane */
    swi (Wimp_OpenWindow, R1, pane, END);

    /* and shade the DELETE button */
    dbox_shade (window->keycutsdbox, I_KEYPARENT_DELETE, TRUE);

    return NULL;
}


/*
 * Loads the fields of the keycut at the given position into the icons of
 *  the keycuts dbox.
 */

static error * load_keycut (WindowObjPtr window, KeyBoardShortcutPtr key)
{
    WindowPtr dbox = window->keycutsdbox;

    dbox_setstring (dbox, I_KEYPARENT_KEY, code_to_keyid (key->wimpkeycode));
    dbox_sethex (dbox, I_KEYPARENT_CODE, key->wimpkeycode);
    gui_put_event (dbox, I_KEYPARENT_DELIVER, I_KEYPARENT_EVENT,
                                                        key->keyevent);
    gui_put_opt_str (dbox, I_KEYPARENT_SHOW, I_KEYPARENT_OBJECT,
                                                        key->keyshow);
    GUI_PUT_FLAG(dbox, I_KEYPARENT_TRANSIENT, key->flags,
                                    KEYBOARDSHORTCUT_SHOWASTRANSIENT);
    dbox_shade (dbox, I_KEYPARENT_TRANSIENT,
                      !dbox_getbutton (dbox, I_KEYPARENT_SHOW));

    return NULL;
}


/*
 * Called from mouse_click(..) in main.c when the user clicks inside the
 *  parent keycuts dialogue box.
 */

error * keycuts_dbox_mouse_click (
    MouseClickPtr mouse,
    unsigned int modifiers,
    WindowObjPtr window
)
{
    int buttons = mouse->buttons;
    int icon = mouse->iconhandle;
    int dir = buttons == MB_CLICK(MB_ADJUST) ? -1 : 1;
    WindowPtr dbox = window->keycutsdbox;
    error *err = NULL;

    if (buttons == MB_CLICK(MB_SELECT) || buttons == MB_CLICK(MB_ADJUST))
    {
        /* indicate whether KEY icon is available for direct input */
        {
            int flags = (icon == I_KEYPARENT_KEY) ? FLAGS_ON : FLAGS_OFF;

            dbox_iconflag (dbox, I_KEYPARENT_KEY, HIGHLIGHT_MASK, flags);

            /* make caret invisible if KEY icon activated */
            if (flags == FLAGS_ON)
                ER ( swi (Wimp_SetCaretPosition,
                             R0, dbox->handle,
                             R1, -1,
                             R4, (1 << 25),
                             R5, 0, END) );
        }

        switch (icon)
        {
        case I_KEYPARENT_CANCEL:
            if (dir == 1)
                err = keycuts_close_dbox (window);
            else
                err = keycuts_update_dbox (window, TRUE, FALSE);
            break;

        case I_KEYPARENT_OK:
            err = keycuts_apply_dbox (window, dir == -1);
            break;

        case I_KEYPARENT_UPDATE:
            err = update_keycuts (window);
            break;

        case I_KEYPARENT_DELETE:
            err = delete_keycuts (window);
            break;

        case I_KEYPARENT_DELIVER:
            GUI_TOGGLE_FADE(dbox, I_KEYPARENT_EVENT);
            if (dbox_getbutton (dbox, I_KEYPARENT_DELIVER))
                dbox_place_caret (dbox, I_KEYPARENT_EVENT);
            break;

        case I_KEYPARENT_SHOW:
            GUI_TOGGLE_FADE(dbox, I_KEYPARENT_OBJECT);
            GUI_TOGGLE_FADE(dbox, I_KEYPARENT_TRANSIENT);
            if (dbox_getbutton (dbox, I_KEYPARENT_SHOW))
                dbox_place_caret (dbox, I_KEYPARENT_OBJECT);
            break;
        }
    }

    /* ensure dbox has caret, and that it is not in a faded icon */
    dbox_set_caret_to (dbox, -1);

    return err;
}


/*
 * Interactor for keycut drag.
 * The expected destination is a menu entry dialogue box; in any case, at
 *  the end of the drag, a KEYCUT_DETAILS message is sent to the task owning
 *  the window into which the keyboard shortcut was dropped.
 */

static error * key_drag_interactor (
    unsigned int event,
    int *buf,
    void *closure,
    Bool *consumed
)
{
    KeyBoardShortcutPtr key = (KeyBoardShortcutPtr) closure;

    if (buf == NULL)            /* we are being asked to cancel */
    {
        /* cancel wimp dragbox operation */
        return swi(Wimp_DragBox,  R1, 0,  END);
    }
    
    switch (event)
    {
    case EV_KEY_PRESSED:
        {
            KeyPressPtr key = (KeyPressPtr) buf;

            /* this drag has priority over keyboard shortcuts */
            *consumed = (key->code != 0x1b);
        }
        break;

    case EV_USER_DRAG_BOX:
        {
            PointerInfoRec mouse;

            /* determine screen location of mouse */
            swi (Wimp_GetPointerInfo, R1, &mouse, END);

            /* send KEYCUT_DETAILS message if user dropped box over window */
            if (mouse.windowhandle >= 0)
            {
                MessageResEdKeycutDetailsRec keycut;
                char *nextname = keycut.names;

                keycut.header.yourref = 0;
                keycut.header.size = sizeof (keycut);
                keycut.header.messageid = MESSAGE_RESED_KEYCUT_DETAILS;
                keycut.flags = 0;
                keycut.shelltaskid = parenttaskhandle;
                keycut.windowhandle = mouse.windowhandle;
                keycut.keycode = key->wimpkeycode;
                strcpy (nextname, code_to_keyid (key->wimpkeycode));
                nextname += (strlen (nextname) + 1);
                if (key->keyshow != NULL)
                {
                    keycut.flags |= KEYCUT_DETAILS_SHOW_OBJECT;
                    strcpy (nextname, key->keyshow);
                    if ((key->flags & KEYBOARDSHORTCUT_SHOWASTRANSIENT) != 0)
                        keycut.flags |= KEYCUT_DETAILS_SHOW_AS_TRANSIENT;
                }
                if (key->keyevent != 0)
                {
                    keycut.flags |= KEYCUT_DETAILS_DELIVER_EVENT;
                    keycut.eventcode = key->keyevent;
                }
                ER ( swi (Wimp_SendMessage,
                          R0, EV_USER_MESSAGE,
                          R1, &keycut,
                          R2, mouse.windowhandle,  END) );
            }

            interactor_cancel();
            *consumed = TRUE;
        }
        break;
    }
    return NULL;
}


/*
 * Initiates a drag of a keyboard shortcut definition.
 */

static error * start_key_drag (
    KeyBoardShortcutPtr key,     /* identifies the keycut definition */
    WindowPtr window,            /* the keycut pane */
    int line,                    /* corresponding line within the pane */
    PointPtr mousepos            /* and position of mouse */
)
{
    DragBoxRec drag;
    int topy = - (line * line_height);

    drag.type = 5;
    drag.initial.minx = window->workarea.minx;
    drag.initial.miny = topy - line_height;
    drag.initial.maxx = window->workarea.maxx;
    drag.initial.maxy = topy;
    wimp_convert_rect (WorkToScreen, window, &drag.initial, &drag.initial);
    drag.constrain.minx = drag.initial.minx - mousepos->x;
    drag.constrain.miny = drag.initial.miny - mousepos->y;
    drag.constrain.maxx = screenx + (drag.initial.maxx - mousepos->x)
                                  - scalex;
    drag.constrain.maxy = screeny + (drag.initial.maxy - mousepos->y)
                                  - scaley;

    ER ( swi (Wimp_DragBox,  R1, &drag,  END) );

    interactor_install (key_drag_interactor, (void *) key);

    return NULL;
}


/*
 * Called from mouse_click(..) in main.c when the user clicks inside the
 *  keycuts pane.
 */

error * keycuts_pane_mouse_click (
    MouseClickPtr mouse,
    unsigned int modifiers,
    WindowObjPtr window
)
{
    int buttons = mouse->buttons;
    WindowPtr pane = window->keycutspane;
    KeyBoardShortcutPtr key = window->keys;
    int i = 0;

    static int line;

    /* for these cases there must have been a previous MB_SINGLECLICK */
    switch (mouse->buttons)
    {
    case MB_DOUBLE(MB_SELECT):  /* keycut must be selected by now */
    case MB_DRAG(MB_SELECT):
        while (key != NULL)
        {
            if (i == line)
                break;
            i++;
            key = key->next;
        }
        if (key != NULL)
        {
            if (mouse->buttons == MB_DOUBLE(MB_SELECT))
                load_keycut (window, key);
            else
                start_key_drag (key, pane, line, &mouse->position);
        }
        return NULL;
    }

    /* determine which line was clicked */
    line = (pane->visarea.maxy -
            (mouse->position.y + 1) -
            pane->scrolloffset.y) / line_height;

    /* nothing to do if outside range */
    if (line >= window->numkeys)
        return NULL;

    switch (buttons)
    {
    case MB_SINGLECLICK(MB_SELECT):
        /* select this one, deselect all others */
        while (key != NULL)
        {
            if ( (key->selected && i != line) ||
                 (i == line && !key->selected) )
            {
                key->selected = !key->selected;
                redraw_line (pane, i);
            }
            i++;
            key = key->next;
        }
        if (window->numkeysselected != 1)
        {
            window->numkeysselected = 1;
            dbox_shade (window->keycutsdbox, I_KEYPARENT_DELETE, FALSE);
        }
        break;

    case MB_SINGLECLICK(MB_ADJUST):
        /* toggle selection of this one */
        while (key != NULL)
        {
            if (i == line)
            {
                key->selected = !key->selected;
                redraw_line (pane, i);
                if (key->selected)
                    window->numkeysselected++;
                else
                    window->numkeysselected--;
                break;
            }
            i++;
            key = key->next;
        }
        dbox_shade (window->keycutsdbox, I_KEYPARENT_DELETE,
                                         window->numkeysselected == 0);
        break;
    }

    return NULL;
}


/*
 * Called from open_window_request(..) in main.c.
 *
 * Copies coordinates of current position to the prototype;
 *  see keycuts_open_dbox(..) above.
 */

error * keycuts_reopen_dbox (WindowPtr win, WindowObjPtr window)
{
    
    window->keycutsdbox->visarea = parentproto->visarea = win->visarea;
    window->keycutsdbox->behind = win->behind;

    ER ( open_pane (window->keycutspane, window->keycutsdbox) );
    return open_parent (window->keycutspane, window->keycutsdbox);
}


/*
 * Called from open_window_request(..) in main.c - presumably only when the
 *  pane's scroll bars are moved.
 */

error * keycuts_reopen_pane (WindowPtr win, WindowObjPtr window)
{
    RectRec pos = * ((RectPtr) &win->visarea);

    /* reopen it */
    ER ( swi (Wimp_OpenWindow, R1, win, END) );

    /* note new vertical scroll offset */
    window->keycutspane->scrolloffset.y = win->scrolloffset.y;

    /* it's (just) possible that the pane may be moved by the Wimp when 
     * it is opened; so we have to check this and re-open its parent if
     * this happens
     */

    if ( pos.minx != win->visarea.minx ||
         pos.miny != win->visarea.miny ||
         pos.maxx != win->visarea.maxx ||
         pos.maxy != win->visarea.maxy )
    {
        WindowPtr pane = window->keycutspane;
        WindowPtr parent = window->keycutsdbox;
        int width = parent->visarea.maxx - parent->visarea.minx;
        int height = parent->visarea.maxy - parent->visarea.miny;

        parent->visarea.minx = pane->visarea.minx;
        parent->visarea.maxy = pane->visarea.maxy;
        parent->visarea.maxx = parent->visarea.minx + width;
        parent->visarea.miny = parent->visarea.maxy - height;

        return open_parent (pane, parent);
    }
    else
        return NULL;
}


/*
 * Called from:
 *   close_window_request(..) in main.c - when the user clicks on the
 *          dialogue box's close icon.
 *   keycuts_apply_dbox(..) -  after the new contents have been processed.
 *   keycuts_dbox_mouse_click(..) -  after a SEL-CANCEL click.
 *   windowedit_close_window(..) - when closing the parent editing window.
 *
 * The dialogue box and its pane are deleted.
 */

error * keycuts_close_dbox (WindowObjPtr window)
{
    WindowPtr keycutsdbox = window->keycutsdbox;
    WindowPtr keycutspane = window->keycutspane;

    /* throw away local copy of keycuts list */
    free_keycuts (window->keys);
    window->keys = NULL;

    /* remove the parent */
    ER ( registry_deregister_window (keycutsdbox->handle) );
    ER ( swi (Wimp_DeleteWindow,  R1, keycutsdbox,  END) );
    free ((char *) keycutsdbox);
    window->keycutsdbox = NULL;

    /* remove the pane */
    ER ( registry_deregister_window (keycutspane->handle) );

    /*
     * This service call is fielded by BorderUtils 0.05 which fixes
     *  a bug associated with the deletion of windows which have pressed-in
     *  slabbed icons in wimps before 3.17
     */

    ER ( swi (OS_ServiceCall, R0, keycutsdbox->handle,
                              R1, 0x44ec5,
                              R2, taskhandle, END) );

    ER ( swi (Wimp_DeleteWindow,  R1, keycutspane,  END) );
    free ((char *) keycutspane);
    window->keycutspane = NULL;

    return windowedit_focus_claim (window);
}


/*
 * Called from key_pressed(..) in main(..), when the user depresses a key
 *  when the dialogue box has the input focus.
 */

error * keycuts_dbox_key_pressed (
    WindowObjPtr window,
    KeyPressPtr key,
    Bool *consumed
)
{
    WindowPtr dbox = window->keycutsdbox;

    /* direct the key press to the KEY icon if this is active */
    if ((dbox_getflags (dbox, I_KEYPARENT_KEY) & HIGHLIGHT_MASK) == FLAGS_ON)
    {
        *consumed = TRUE;

        dbox_setstring (dbox, I_KEYPARENT_KEY, code_to_keyid (key->code));
        dbox_sethex (dbox, I_KEYPARENT_CODE, key->code);

        /* de-activate KEY icon */
        dbox_iconflag (dbox, I_KEYPARENT_KEY, HIGHLIGHT_MASK, FLAGS_OFF);

        /* place caret in EVENT or OBJECT icon if one of these is active */
        {
            int icon = -1;

            if (dbox_getbutton (dbox, I_KEYPARENT_SHOW))
                icon = I_KEYPARENT_OBJECT;
            if (dbox_getbutton (dbox, I_KEYPARENT_DELIVER))
                icon = I_KEYPARENT_EVENT;

            if (icon >= 0)
                ER ( swi (Wimp_SetCaretPosition,
                             R0, dbox->handle,
                             R1, icon,
                             R4, -1,
                             R5, strlen (dbox_getstring (dbox, icon)),
                             END ) );
        }

        return NULL;
    }

    if (key->code == 13)         /* RETURN */
    {
        *consumed = TRUE;
        keycuts_apply_dbox (window,
                            (wimp_read_modifiers() & MODIFIER_SHIFT) != 0);
    }

    else if (key->code == 0x1b)  /* ESCAPE */
    {
        *consumed = TRUE;
        if ((wimp_read_modifiers() & MODIFIER_SHIFT) == 0)
            return keycuts_close_dbox (window);
        else
            return keycuts_update_dbox (window, TRUE, FALSE);
    }

    return NULL;
}


/*
 * Construct details of the key board shortcut 'key' in the string 's'
 */

static void get_keycut (KeyBoardShortcutPtr key, char *s)
{
    Bool hasevent = key->keyevent != 0;
    Bool hasshow = key->keyshow != NULL;
    Bool transient = (key->flags & KEYBOARDSHORTCUT_SHOWASTRANSIENT) != 0;

    sprintf (s, "Key &%03x (%s) -> ", key->wimpkeycode,
                                      code_to_keyid (key->wimpkeycode));
    if (hasevent)
    {
        s += strlen (s);
        sprintf (s, "Event &%x%s", key->keyevent, (hasshow) ? ", " : "");
    }
    if (hasshow)
    {
        s += strlen (s);
        sprintf(s, "Show \"%s\"%s", key->keyshow, (transient) ? " (T)" : "");
    }

    return;
}


/*
 * Redraw loop for the keycuts pane.
 */

error * keycuts_redraw_pane (
    WindowRedrawPtr redraw,
    WindowObjPtr window
)
{
    int more;
    char s[100];    /* large enough to hold one line of the pane */

    /* set foreground and background colours for plotting */
    swi (Wimp_SetColour, R0, panefg, END);        /* fg colour */
    swi (Wimp_SetColour, R0, panebg + 128, END);  /* bg colour */

    ER ( swi(Wimp_RedrawWindow,  R1, redraw,  OUT,  R0, &more,  END) );
    while (more)
    {
        int topspot, topline, bottomline, ploty, i;
        KeyBoardShortcutPtr key;

        /* work area y-coordinate of top line of pixels in redraw area */
        topspot = redraw->visarea.maxy -
                  redraw->graphwin.maxy -
                  redraw->scrolloffset.y;

        /* index of line which falls on this top line of pixels */
        topline = topspot / line_height;

        /* index of line which falls on the bottom line of pixels in the
           redraw area */
        bottomline = (redraw->visarea.maxy -
                      redraw->graphwin.miny -
                      redraw->scrolloffset.y - 1) / line_height;

        /* screen area y-coordinate of line to contain the top line of pixels
           of the characters of 'topline' */
        ploty = redraw->graphwin.maxy +
                (topspot % line_height) - 1;

        i = 0;
        key = window->keys;
        while (i <= bottomline && key != NULL)
        {
            /* these are the ones to display */
            if (i >= topline)
            {
                /* construct string for display */
                get_keycut(key, s);

                if (key->selected)
                {
                    /* move to top-left pixel of first character of line */
                    swi (OS_Plot, R0, 4,   /* move absolute */
                                  R1, redraw->visarea.minx,
                                  R2, ploty, END);

                    /* rectangle-fill this line in the foreground colour */
                    swi (OS_Plot, R0, 5+96,
                                  R1, redraw->visarea.maxx,
                                  R2, ploty - (line_height - 1), END);

                    /* plot keycut details in background colour */
                    swi (Wimp_SetColour, R0, panebg, END);       /* set fg */
                    swi (Wimp_SetColour, R0, panefg + 128, END); /* set bg */

                    if (usetextop)
                        swi (Wimp_TextOp, R0, 2 | BIT(30),
                                          R1, s,
                                          R2, -1,
                                          R3, -1,
                                          R4, redraw->visarea.minx,
                                          R5, ploty - (line_height - 1) +
                                                      TEXT_OP_FUDGE,
                                      END);
                    else
                    {
                        /* move to the top-left pixel of first char */
                        swi (OS_Plot, R0, 4,
                                      R1, redraw->visarea.minx,
                                      R2, ploty, END);

                        /* and plot the string */
                        swi (OS_Write0, R0, s, END);
                    }

                    swi (Wimp_SetColour, R0, panefg, END);
                    swi (Wimp_SetColour, R0, panebg + 128, END);
                }
                else
                {
                    if (usetextop)
                        swi (Wimp_TextOp, R0, 2 | BIT(30),
                                          R1, s,
                                          R2, -1,
                                          R3, -1,
                                          R4, redraw->visarea.minx,
                                          R5, ploty - (line_height - 1) +
                                                      TEXT_OP_FUDGE,
                                      END);
                    else
                    {
                        /* move to the top-left pixel of first char */
                        swi (OS_Plot, R0, 4,
                                      R1, redraw->visarea.minx,
                                      R2, ploty, END);

                        /* and plot the string */
                        swi (OS_Write0, R0, s, END);
                    }
                }

                /* move down ready for next line */
                ploty -= line_height;
            }

            i++;
            key = key->next;
        }

        ER ( swi (Wimp_GetRectangle,  R1,  redraw,  OUT,  R0, &more,  END) );
    }

    return NULL;
}


/*
 * Returns TRUE if 'icon' is an acceptable place to drop an object onto in
 *  a keycuts dialogue box.
 *
 * Called from protocol_send_resed_object_name_request(..).
 */

Bool keycuts_drop_icon (
    int icon
)
{
    switch (icon)
    {
    case I_KEYPARENT_SHOW:
    case I_KEYPARENT_OBJECT:
        return TRUE;
    }

    return FALSE;
}


/*
 * Called to process an object of class 'class', name 'name', dropped onto
 *  icon 'icon' in the keycuts dbox of 'window'.
 *
 * Called from received_resed_object_name(..) in c.protocol.
 */

error * keycuts_object_drop (
    WindowObjPtr window,
    int icon,
    ObjectClass class,
    char *name
)
{
    WindowPtr dbox = window->keycutsdbox;

    switch (icon)
    {
    case I_KEYPARENT_SHOW:
    case I_KEYPARENT_OBJECT:
        ER ( gui_put_opt_str (dbox, I_KEYPARENT_SHOW, I_KEYPARENT_OBJECT,
                              name) );
        return dbox_shade (dbox, I_KEYPARENT_TRANSIENT, FALSE);
    }

    return NULL;
}
